using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using Tycho.Grammar;
using Tycho.Parser.Peg;

namespace Tycho.UnitTests.Grammar {
    [TestFixture]
    public class GrammarCompilerCompileProductionSpecification {
        [Test]
        public void ShouldCompileSimpleNonTerminal () {
            var evaluator = new IdentifierProductionCompilerEvaluator (IdentifierProduction.CreateTerminal ());

            var lookup = new Mock<IProductionLookup> ();
            lookup.Setup (l => l.Lookup ("id")).Returns (IdentifierProduction.CreateTerminal ());
            lookup.Setup (l => l.Lookup ("int")).Returns (IntegerProduction.CreateTerminal ());

            IParser production = CompileProduction ("if {'if' condition: id then: int ((';')? 'else' else: int)?}", evaluator, lookup.Object);

            AssertIfStatement (production, "if true 4 else 5", "true", 4, 5);
            AssertIfStatement (production, "if true 4; else 5", "true", 4, 5);
            AssertIfStatement (production, "if true 4\nelse 5", "true", 4, 5);
            AssertIfStatement (production, "if true 4\r\nelse 5", "true", 4, 5);
            AssertIfStatement (production, "if true 4", "true", 4, null);
            AssertNotParsed (production, "if true 4 4");
            AssertNotParsed (production, "if true else 5");
            AssertNotParsed (production, "if true 4\n else 5");
        }

        static void AssertIfStatement (IParser ifProduction, string source, string condition, int thenBlock, int? elseBlock) {
            ITerm term = ifProduction.ParseTerm (source);
            Assert.IsNotNull (term);

            var ifTerm = term as CompositeTerm;
            Assert.IsNotNull (ifTerm);
            Assert.AreEqual ("if", ifTerm.Name);

            var conditionIdentifier = ifTerm.SubTerms["condition"] as IdentifierTerm;
            Assert.IsNotNull (conditionIdentifier);
            Assert.AreEqual (condition, conditionIdentifier.Name);

            var thenBlockInteger = ifTerm.SubTerms["then"] as IntegerTerm;
            Assert.IsNotNull (thenBlockInteger);
            Assert.AreEqual (thenBlock, thenBlockInteger.Value);

            if (elseBlock.HasValue) {
                var elseBlockInteger = ifTerm.SubTerms["else"] as IntegerTerm;
                Assert.IsNotNull (elseBlockInteger);
                Assert.AreEqual (elseBlock, elseBlockInteger.Value);
            } else {
                Assert.IsFalse (ifTerm.SubTerms.ContainsKey ("else"));
            }
        }

        [Test]
        public void ShouldLookupProductionNameForIdentifier () {
            var lookup = new Mock<IProductionLookup> ();

            lookup.Setup (l => l.Lookup ("identifier")).Returns (IdentifierProduction.CreateTerminal ());

            IParser termParser = CompileProduction ("identifier", lookup.Object);

            AssertIdentifier (termParser, "a", "a");
            AssertIdentifier (termParser, "a-a", " a-a");
            AssertIdentifier (termParser, "a", "a ");
            AssertNotParsed (termParser, "8");
        }

        [Test]
        public void ShouldCompileString () {
            var evaluator = new Mock<IProductionCompilerEnvironment> ();
            evaluator.SetupGet (e => e.Default).Returns (IntegerProduction.CreateTerminal ());

            IParser production = CompileProduction ("string", evaluator.Object, null);

            AssertString (production, "a", "'a'");
            AssertString (production, "b", "\"b\"");
            AssertString (production, "\t\n", "\"\\t\\n\"");
            AssertString (production, "a\"b", "\"a\\\"b\"");
            AssertInterpolatedString (production, new object[] {"one", 2, "three", 4}, "\"one#2three#4\"");
            AssertNotParsed (production, "\"");
            AssertNotParsed (production, "'");
        }

        [Test]
        public void ShouldCompileInterpolatedString () {
            var lookup = new Mock<IProductionLookup> ();
            lookup.Setup (l => l.Lookup ("real")).Returns (FloatingPointProduction.CreateTerminal ());

            IParser termParser = CompileProduction ("string real", lookup.Object);

            AssertInterpolatedString (termParser, new object[] {"one", 2.4, "three", 5.3, "six"}, "\"one#2.4three#5.3six\"");
        }

        [Test]
        public void ShouldCompileQuotedExpression () {
            var evaluator = new IdentifierProductionCompilerEvaluator (IntegerProduction.CreateTerminal ());
            evaluator.Add ("int", IntegerProduction.CreateTerminal ());

            IParser production = CompileProduction ("#int", evaluator, null);

            AssertInteger (production, 3, "3");
        }

        [Test]
        public void ShouldCompileDynamicChoice () {
            var lookup = new Mock<IProductionLookup> ();
            lookup.Setup (l => l.Lookup ("real")).Returns (FloatingPointProduction.CreateTerminal ());
            lookup.Setup (l => l.Lookup ("integer")).Returns (IntegerProduction.CreateTerminal ());

            IParser production = CompileProduction ("choice {real, integer}", lookup.Object);

            AssertInteger (production, 3, "3");
            AssertReal (production, 3.3, "3.3");
            AssertNotParsed (production, "a");
        }

        private static void AssertNotParsed (IParser production, string source) {
            Assert.That (() => { production.ParseTerm (source); }, Throws.TypeOf(typeof (SyntaxException)));
        }

        private static void AssertInteger (IParser production, int expected, string source) {
            ITerm term = production.ParseTerm (source);
            Assert.IsNotNull (term);
            var id = term as IntegerTerm;
            Assert.IsNotNull (id);
            Assert.AreEqual (expected, id.Value);
        }

        private static void AssertReal (IParser production, double expected, string source) {
            ITerm term = production.ParseTerm (source);
            Assert.IsNotNull (term);
            var id = term as FloatingPointTerm;
            Assert.IsNotNull (id);
            Assert.AreEqual (expected, id.Value);
        }

        private static void AssertString (IParser production, string expected, string source) {
            ITerm term = production.ParseTerm (source);
            Assert.IsNotNull (term);
            var id = term as StringTerm;
            Assert.IsNotNull (id);
            Assert.AreEqual (expected, id.Value);
        }

        private void AssertInterpolatedString (IParser production, object [] expected, string source) {
            ITerm term = production.ParseTerm (source);
            Assert.IsNotNull (term);
            var id = term as CompositeTerm;
            Assert.IsNotNull (id);
            Assert.That (id.Name, Is.EqualTo ("interpolated-string"));
            IEnumerable<object> actual = ComponentTermsToValues (id);
            Assert.That (actual, Is.EqualTo (expected));
        }

        private static IEnumerable<object> ComponentTermsToValues (CompositeTerm id) {
            return ((ListTerm) id.SubTerms["components"]).Terms.Select (t => TermToValue (t));
        }

        private static object TermToValue (ITerm t) {
            var str = t as StringTerm;
            if (str != null) {
                return str.Value;
            }
            var i = t as IntegerTerm;
            if (i != null) {
                return i.Value;
            }
            var f = t as FloatingPointTerm;
            if (f != null) {
                return f.Value;
            }

            return null;
        }

        private static void AssertIdentifier (IParser production, string identifier, string source) {
            ITerm term = production.ParseTerm (source);
            Assert.IsNotNull (term);
            var id = term as IdentifierTerm;
            Assert.IsNotNull (id);
            Assert.AreEqual (identifier, id.Name);
        }

        private static IParser CompileProduction (string source, IProductionLookup lookup) {
            return CompileProduction (source, null, lookup);
        }

        private static IParser CompileProduction (string source, IProductionCompilerEnvironment environment, IProductionLookup lookup) {
            IProduction nonTerminalProduction = GrammarLoader.CreateProduction ();
            ITerm term = nonTerminalProduction.ParseTerm (source);
            IDynamicGrammar grammar = new LazyDynamicGrammar ();
            var grammarCompiler = new GrammarCompiler ();
            GrammarTree grammarTree = grammarCompiler.Compile (term, environment, lookup, grammar);

            return new DynamicParser (grammar, grammarTree.Production);
        }
    }
}